Add ostree admin prepare-soft-reboot
authorColin Walters <walters@verbum.org>
Wed, 18 Jun 2025 19:10:53 +0000 (15:10 -0400)
committerColin Walters <walters@verbum.org>
Thu, 26 Jun 2025 16:28:19 +0000 (12:28 -0400)
This adds support for systemd soft reboots.

Closes: https://github.com/ostreedev/ostree/issues/3242
Signed-off-by: Colin Walters <walters@verbum.org>
Co-authored-by: Joseph Marrero Corchado <jmarrero@redhat.com>
Co-authored-by: Mary Strodl <ipadlover8322@gmail.com>
Signed-off-by: Colin Walters <walters@verbum.org>
24 files changed:
Makefile-libostree.am
Makefile-man.am
Makefile-ostree.am
apidoc/ostree-sections.txt
man/ostree-admin-prepare-soft-reboot.xml [new file with mode: 0644]
src/boot/ostree-boot-complete.service
src/libostree/libostree-devel.sym
src/libostree/ostree-cmd-private.c
src/libostree/ostree-cmd-private.h
src/libostree/ostree-deployment-private.h
src/libostree/ostree-deployment.c
src/libostree/ostree-deployment.h
src/libostree/ostree-soft-reboot.c [new file with mode: 0644]
src/libostree/ostree-sysroot-deploy.c
src/libostree/ostree-sysroot-private.h
src/libostree/ostree-sysroot.c
src/libostree/ostree-sysroot.h
src/libotcore/otcore.h
src/ostree/ot-admin-builtin-impl-prepare-soft-reboot.c [new file with mode: 0644]
src/ostree/ot-admin-builtin-prepare-soft-reboot.c [new file with mode: 0644]
src/ostree/ot-admin-builtin-status.c
src/ostree/ot-admin-builtins.h
src/ostree/ot-builtin-admin.c
tests/kolainst/destructive/soft-reboot.sh [new file with mode: 0755]

index b84ac0e5a17edf6fbe76cb411729ad347d0d0397..463b809a1cdae44b8d7f19883688f1cce5f2c670 100644 (file)
@@ -107,6 +107,7 @@ libostree_1_la_SOURCES = \
        src/libostree/ostree-sysroot-cleanup.c \
        src/libostree/ostree-sysroot-deploy.c \
        src/libostree/ostree-sysroot-upgrader.c \
+       src/libostree/ostree-soft-reboot.c \
        src/libostree/ostree-impl-system-generator.c \
        src/libostree/ostree-bootconfig-parser.c \
        src/libostree/ostree-deployment.c \
@@ -175,9 +176,9 @@ endif # USE_GPGME
 symbol_files = $(top_srcdir)/src/libostree/libostree-released.sym
 
 # Uncomment this include when adding new development symbols.
-#if BUILDOPT_IS_DEVEL_BUILD
-#symbol_files += $(top_srcdir)/src/libostree/libostree-devel.sym
-#endif
+if BUILDOPT_IS_DEVEL_BUILD
+symbol_files += $(top_srcdir)/src/libostree/libostree-devel.sym
+endif
 
 # http://blog.jgc.org/2007/06/escaping-comma-and-space-in-gnu-make.html
 wl_versionscript_arg = -Wl,--version-script=
index 5d1b9d4822901aad4ba2cbaedbd76c6a90e7d7ee..b36f3af144147b841534e45b2510cb6239f8686b 100644 (file)
@@ -30,6 +30,7 @@ ostree-admin-init-fs.1 ostree-admin-instutil.1 ostree-admin-stateroot-init.1 ost
 ostree-admin-status.1 ostree-admin-set-origin.1 ostree-admin-switch.1  \
 ostree-admin-undeploy.1 ostree-admin-upgrade.1 ostree-admin-unlock.1   \
 ostree-admin-pin.1 ostree-admin-post-copy.1 ostree-admin-set-default.1 \
+ostree-admin-prepare-soft-reboot.1 \
 ostree-admin-lock-finalization.1 \
 ostree-admin.1 ostree-cat.1 ostree-checkout.1 ostree-checksum.1                \
 ostree-commit.1 ostree-create-usb.1 ostree-export.1 \
index d2447ffe9d5a7625234909425116841154ad6f73..6076ae51ed908d2ca4b068f533ea9fbf5d3cc027 100644 (file)
@@ -83,6 +83,8 @@ ostree_SOURCES += \
        src/ostree/ot-admin-builtin-switch.c \
        src/ostree/ot-admin-builtin-pin.c \
        src/ostree/ot-admin-builtin-post-copy.c \
+       src/ostree/ot-admin-builtin-impl-prepare-soft-reboot.c \
+       src/ostree/ot-admin-builtin-prepare-soft-reboot.c \
        src/ostree/ot-admin-builtin-upgrade.c \
        src/ostree/ot-admin-builtin-unlock.c \
        src/ostree/ot-admin-builtin-state-overlay.c \
index 3a46d7e08e353fa92784edf849d7b15a3c595014..2ad402cb5146199e8e62821ef09837ed0e1e2e4a 100644 (file)
@@ -192,6 +192,7 @@ ostree_deployment_get_unlocked
 ostree_deployment_is_pinned
 ostree_deployment_is_staged
 ostree_deployment_is_finalization_locked
+ostree_deployment_is_soft_reboot_target
 ostree_deployment_set_index
 ostree_deployment_set_bootserial
 ostree_deployment_set_bootconfig
@@ -589,6 +590,8 @@ ostree_sysroot_deployment_set_kargs_in_place
 ostree_sysroot_deployment_set_mutable
 ostree_sysroot_deployment_unlock
 ostree_sysroot_deployment_set_pinned
+ostree_sysroot_deployment_can_soft_reboot
+ostree_sysroot_deployment_prepare_next_root
 ostree_sysroot_write_deployments
 ostree_sysroot_write_deployments_with_options
 ostree_sysroot_write_origin_file
diff --git a/man/ostree-admin-prepare-soft-reboot.xml b/man/ostree-admin-prepare-soft-reboot.xml
new file mode 100644 (file)
index 0000000..fa34457
--- /dev/null
@@ -0,0 +1,51 @@
+<?xml version='1.0'?> <!--*-nxml-*-->
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
+    "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd">
+
+<!--
+SPDX-License-Identifier: LGPL-2.0+
+-->
+
+<refentry id="ostree">
+
+    <refentryinfo>
+        <title>ostree admin prepare-soft-reboot</title>
+        <productname>OSTree</productname>
+    </refentryinfo>
+
+    <refmeta>
+        <refentrytitle>ostree admin prepare-soft-reboot</refentrytitle>
+        <manvolnum>1</manvolnum>
+    </refmeta>
+
+    <refnamediv>
+        <refname>ostree-admin-prepare-soft-reboot</refname>
+        <refpurpose>Prepare the target deployment (via index) for soft reboot</refpurpose>
+    </refnamediv>
+
+    <refsynopsisdiv>
+            <cmdsynopsis>
+                <command>ostree admin prepare-soft-reboot</command> <arg choice="req">INDEX</arg>
+            </cmdsynopsis>
+    </refsynopsisdiv>
+
+    <refsect1>
+        <title>Description</title>
+
+        <para>
+            Prepare the deployment at INDEX for a systemd soft reboot. INDEX must be in range and not reference the currently booted deployment.
+            It is recommended to immediately follow this with an involcation of <command>systemctl soft-reboot</command>.
+        </para>
+
+        <para>
+            It is not supported to soft reboot into a deployment with a different kernel than the booted one.
+        </para>
+    </refsect1>
+
+  <refsect1>
+    <title>See Also</title>
+    <para><simplelist type="inline">
+      <member><citerefentry><refentrytitle>systemd-soft-reboot.service</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
+    </simplelist></para>
+  </refsect1>
+</refentry>
index 98ae3d181da24a411269a76d5a5cdc1c797a2078..e8b4f42f6a56704cc78131ec943b45cbd6a5cf32 100644 (file)
@@ -21,6 +21,8 @@ ConditionKernelCommandLine=ostree
 # marked as a triggering condition in case in the future we want
 # to do something else.
 ConditionPathExists=|/boot/ostree/finalize-failure.stamp
+# Also run when soft-reboot cleanup is needed
+ConditionPathExists=|/run/ostree/nextroot-booted
 # We start early
 DefaultDependencies=no
 After=sysinit.target
index 6640e11c78d7a370a8191ff9a97b285a29df0b8f..4a46a0b399876aaf3f51fc1f14ddffe1c45ae0f8 100644 (file)
@@ -29,3 +29,10 @@ global:
   someostree_symbol_deleteme;
 } LIBOSTREE_$YEAR.$LASTSTABLE;
 */
+
+LIBOSTREE_2025.3 {
+global:
+  ostree_deployment_is_soft_reboot_target;
+  ostree_sysroot_deployment_can_soft_reboot;
+  ostree_sysroot_deployment_prepare_next_root;
+} LIBOSTREE_2025.2;
index 239d3cd3f54a9cb445d0fc60ed1ab2aace7a5786..f40d3daafd93b3855d4bf9eade60fab9be692a23 100644 (file)
@@ -50,6 +50,7 @@ ostree_cmd__private__ (void)
     _ostree_repo_static_delta_dump,   _ostree_repo_static_delta_query_exists,
     _ostree_repo_static_delta_delete, _ostree_repo_verify_bindings,
     _ostree_sysroot_finalize_staged,  _ostree_sysroot_boot_complete,
+    _ostree_prepare_soft_reboot,
   };
 
   return &table;
index 3b48e6eee960256d40d680ff120d88ec2cf4c2d9..9bd7c4af8fd12b3bb92afb1ddb46619d8f6edba4 100644 (file)
@@ -45,6 +45,7 @@ typedef struct
                                       GError **error);
   gboolean (*ostree_boot_complete) (OstreeSysroot *sysroot, GCancellable *cancellable,
                                     GError **error);
+  gboolean (*ostree_prepare_soft_reboot) (GError **error);
 } OstreeCmdPrivateVTable;
 
 /* Note this not really "public", we just export the symbol, but not the header */
index f6766c39bdef14d43cc9348313dd9612d7f9cdb8..55a3bded0f3bb3e111db3d16b33d8570699a2996 100644 (file)
@@ -52,6 +52,7 @@ struct _OstreeDeployment
   OstreeDeploymentUnlockedState unlocked;
   gboolean staged;
   gboolean finalization_locked;
+  gboolean soft_reboot_target;
   char **overlay_initrds;
   char *overlay_initrds_id;
 };
index 8be2fdd507e82e3ef798c63f2ea6021bba7bc34b..b4427262cfbf4d5de4b7cb04bf78b024fd0fd52a 100644 (file)
@@ -476,3 +476,16 @@ ostree_deployment_is_finalization_locked (OstreeDeployment *self)
 {
   return self->finalization_locked;
 }
+
+/**
+ * ostree_deployment_is_soft_reboot_target:
+ * @self: Deployment
+ *
+ * Returns: `TRUE` if deployment is set for a soft reboot.
+ * Since: TODO
+ */
+gboolean
+ostree_deployment_is_soft_reboot_target (OstreeDeployment *self)
+{
+  return self->soft_reboot_target;
+}
index 0536d9810ce6b5220ea40b3c57c6edea641419ad..7e923fe8955aea3cdaf15759a3fbe93643f140f4 100644 (file)
@@ -73,6 +73,8 @@ gboolean ostree_deployment_is_staged (OstreeDeployment *self);
 _OSTREE_PUBLIC
 gboolean ostree_deployment_is_finalization_locked (OstreeDeployment *self);
 _OSTREE_PUBLIC
+gboolean ostree_deployment_is_soft_reboot_target (OstreeDeployment *self);
+_OSTREE_PUBLIC
 gboolean ostree_deployment_is_pinned (OstreeDeployment *self);
 
 _OSTREE_PUBLIC
diff --git a/src/libostree/ostree-soft-reboot.c b/src/libostree/ostree-soft-reboot.c
new file mode 100644 (file)
index 0000000..08fc909
--- /dev/null
@@ -0,0 +1,99 @@
+/* -*- c-file-style: "gnu" -*-
+ * Soft reboot for ostree. This code was originally derived from ostree-prepare-root.c,
+ * but is now significantly cut down to target specifically soft rebooting.
+ *
+ * SPDX-License-Identifier: LGPL-2.0+
+ */
+
+#include "config.h"
+
+#include <ctype.h>
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <libglnx.h>
+#include <linux/magic.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/mount.h>
+#include <sys/param.h>
+#include <sys/stat.h>
+#include <sys/statfs.h>
+#include <sys/syscall.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <ostree-core.h>
+#include <ostree-sysroot-private.h>
+
+#include "ostree-mount-util.h"
+#include "ot-keyfile-utils.h"
+#include "otcore.h"
+
+/* This key configures the / mount in the deployment root */
+#define ROOT_KEY "root"
+#define ETC_KEY "etc"
+#define TRANSIENT_KEY "transient"
+
+gboolean
+_ostree_prepare_soft_reboot (GError **error)
+{
+  const char *sysroot_path = "/sysroot";
+  const char *target_deployment = ".";
+
+  g_autoptr (GKeyFile) config = otcore_load_config (AT_FDCWD, PREPARE_ROOT_CONFIG_PATH, error);
+  if (!config)
+    return FALSE;
+
+  gboolean root_transient = FALSE;
+  if (!ot_keyfile_get_boolean_with_default (config, ROOT_KEY, TRANSIENT_KEY, FALSE, &root_transient,
+                                            error))
+    return FALSE;
+
+  g_autofree char *kernel_cmdline = read_proc_cmdline ();
+  g_autoptr (ComposefsConfig) composefs_config
+      = otcore_load_composefs_config (kernel_cmdline, config, TRUE, error);
+  if (!composefs_config)
+    return FALSE;
+
+  if (composefs_config->enabled != OT_TRISTATE_YES)
+    return glnx_throw (error, "soft reboot not supported without composefs");
+
+  GVariantBuilder metadata_builder;
+  g_variant_builder_init (&metadata_builder, G_VARIANT_TYPE ("a{sv}"));
+
+  if (!glnx_shutil_mkdir_p_at (AT_FDCWD, OTCORE_RUN_NEXTROOT, 0755, NULL, error))
+    return FALSE;
+
+  // Tracks if we did successfully enable it at runtime
+  bool using_composefs = false;
+  if (!otcore_mount_rootfs (composefs_config, &metadata_builder, root_transient, sysroot_path,
+                            target_deployment, OTCORE_RUN_NEXTROOT, &using_composefs, error))
+    return glnx_prefix_error (error, "failed to mount composefs");
+
+  if (!using_composefs)
+    return glnx_throw (error, "failed to mount with composefs");
+
+  if (!otcore_mount_etc (config, &metadata_builder, OTCORE_RUN_NEXTROOT, error))
+    return FALSE;
+
+  // Note we should have inherited the readonly sysroot
+  g_autofree char *target_sysroot = g_build_filename (OTCORE_RUN_NEXTROOT, "sysroot", NULL);
+  if (mount (sysroot_path, target_sysroot, NULL, MS_BIND | MS_SILENT, NULL) < 0)
+    return glnx_throw_errno_prefix (error, "failed to bind mount sysroot");
+
+  /* This can be used by other things to signal ostree is in use */
+  {
+    g_autoptr (GVariant) metadata = g_variant_ref_sink (g_variant_builder_end (&metadata_builder));
+    const guint8 *buf = g_variant_get_data (metadata) ?: (guint8 *)"";
+    if (!glnx_file_replace_contents_at (AT_FDCWD, OTCORE_RUN_NEXTROOT_BOOTED, buf,
+                                        g_variant_get_size (metadata), 0, NULL, error))
+      return FALSE;
+  }
+
+  return TRUE;
+}
index a57a812d6f6dec192c12ed4c1f62baab2fe909e6..2d676be15aad27d0a31c28ca19981cacdad746bf 100644 (file)
@@ -4108,10 +4108,20 @@ _ostree_sysroot_boot_complete (OstreeSysroot *self, GCancellable *cancellable, G
   if (!ot_openat_ignore_enoent (self->boot_fd, _OSTREE_FINALIZE_STAGED_FAILURE_PATH, &failure_fd,
                                 error))
     return FALSE;
-  // If we didn't find a failure log, then there's nothing to do right now.
-  // (Actually this unit shouldn't even be invoked, but we may do more in the future)
+  // If we didn't find a failure log, check for soft-reboot completion tasks
   if (failure_fd == -1)
-    return TRUE;
+    {
+      // Check if we just completed a soft-reboot and need to update /run/ostree-booted
+      // We're completing a soft-reboot, simply move the nextroot-booted file to ostree-booted
+      if (rename (OTCORE_RUN_NEXTROOT_BOOTED, OTCORE_RUN_BOOTED) < 0)
+        {
+          if (errno != ENOENT)
+            return glnx_throw_errno_prefix (error, "Failed to rename %s to %s",
+                                            OTCORE_RUN_NEXTROOT_BOOTED, OTCORE_RUN_BOOTED);
+          g_debug ("Updated /run/ostree-booted for soft-reboot completion");
+        }
+      return TRUE;
+    }
   g_autofree char *failure_data = glnx_fd_readall_utf8 (failure_fd, NULL, cancellable, error);
   if (failure_data == NULL)
     return glnx_prefix_error (error, "Reading from %s", _OSTREE_FINALIZE_STAGED_FAILURE_PATH);
@@ -4270,6 +4280,119 @@ ostree_sysroot_deployment_set_mutable (OstreeSysroot *self, OstreeDeployment *de
   return TRUE;
 }
 
+struct PrepareRootChildSetupContext
+{
+  const char *deployment_path;
+  int rootns_fd;
+};
+
+static inline void
+prepare_root_child_setup (gpointer data)
+{
+  struct PrepareRootChildSetupContext *ctx = data;
+  // Enter the root namespace first to escape the overlayfs context
+  int rc = setns (ctx->rootns_fd, CLONE_NEWNS);
+  if (rc < 0)
+    err (1, "setns");
+  // Then change to the deployment directory in the root namespace
+  rc = chdir (ctx->deployment_path);
+  if (rc < 0)
+    err (1, "chdir");
+}
+
+/**
+ * ostree_sysroot_deployment_can_soft_reboot:
+ * @self: The #OstreeSysroot object.
+ * @deployment: The #OstreeDeployment to check for soft-reboot compatibility.
+ *
+ * Checks if the given deployment can be soft-rebooted to from the currently
+ * booted deployment. A soft-reboot is generally only possible if both the
+ * currently booted deployment and the target `deployment` use the same kernel
+ * (i.e., have the same boot checksum).
+ *
+ * Returns: %TRUE if a soft-reboot is possible to the target deployment, %FALSE otherwise.
+ * Since: TODO
+ */
+gboolean
+ostree_sysroot_deployment_can_soft_reboot (OstreeSysroot *self, OstreeDeployment *deployment)
+{
+  OstreeDeployment *booted_deployment = ostree_sysroot_get_booted_deployment (self);
+  if (booted_deployment != NULL)
+    {
+      const char *booted_bootcsum = ostree_deployment_get_bootcsum (booted_deployment);
+      const char *target_bootcsum = ostree_deployment_get_bootcsum (deployment);
+      return g_str_equal (booted_bootcsum, target_bootcsum);
+    }
+  return false;
+}
+
+/**
+ * ostree_sysroot_deployment_prepare_next_root
+ * @self: Sysroot
+ * @deployment: Deployment to prepare /run/nextroot
+ * @allow_kernel_skew: Continue even if there is a kernel mismatch
+ * @cancellable: Cancellable
+ * @error: Error
+ *
+ * Prepare the specified deployment for a systemd soft-reboot by creating a new
+ * root with it at `/run/nextroot`.
+ *
+ * Since: TODO
+ */
+gboolean
+ostree_sysroot_deployment_prepare_next_root (OstreeSysroot *self, OstreeDeployment *deployment,
+                                             gboolean allow_kernel_skew, GCancellable *cancellable,
+                                             GError **error)
+{
+  GLNX_AUTO_PREFIX_ERROR ("Preparing /run/nextroot for a soft-reboot", error);
+
+  if (!ostree_sysroot_deployment_can_soft_reboot (self, deployment) && !allow_kernel_skew)
+    {
+      return glnx_throw (error, "Cannot soft-reboot to deployment with different kernel");
+    }
+
+  // For targeting a staged deployment, we finalize now to ensure that we have /etc
+  if (ostree_deployment_is_staged (deployment))
+    {
+      if (!_ostree_sysroot_finalize_staged (self, NULL, error))
+        return FALSE;
+    }
+
+  g_autofree char *deployment_relpath = ostree_sysroot_get_deployment_dirpath (self, deployment);
+  g_autofree char *deployment_fullpath = g_build_filename ("/sysroot", deployment_relpath, NULL);
+  gint estatus;
+
+  const char *argv[] = { "ostree", "admin", "impl-prepare-soft-reboot", NULL };
+
+  glnx_autofd int rootns_fd = -1;
+  if (!glnx_openat_rdonly (AT_FDCWD, "/proc/1/ns/mnt", TRUE, &rootns_fd, error))
+    return FALSE;
+
+  struct PrepareRootChildSetupContext ctx = {
+    .deployment_path = deployment_fullpath,
+    .rootns_fd = rootns_fd,
+  };
+
+  if (!g_spawn_sync (NULL, (char **)argv, NULL, G_SPAWN_SEARCH_PATH, prepare_root_child_setup, &ctx,
+                     NULL, NULL, &estatus, error))
+    return FALSE;
+
+  if (!g_spawn_check_exit_status (estatus, error))
+    {
+      int flags = G_SPAWN_SEARCH_PATH | G_SPAWN_STDERR_TO_DEV_NULL;
+      // If we failed to initialize the soft reboot, ensure that we've unwound any mounts
+      const char *umount_argv[] = { "umount", "-R", "/run/nextroot", NULL };
+      // To aid debugging allow skipping cleanup on failure
+      if (!g_getenv ("OSTREE_SKIP_NEXTROOT_CLEANUP"))
+        g_spawn_sync (NULL, (char **)umount_argv, NULL, flags, NULL, NULL, NULL, NULL, NULL, NULL);
+      return FALSE;
+    }
+
+  ot_journal_print (LOG_INFO, "Set up soft reboot at /run/nextroot");
+
+  return TRUE;
+}
+
 /**
  * ostree_sysroot_deployment_kexec_load
  * @self: Sysroot
index 9a6566d5ad0cabd391c60f150e988efc30a85811..6370592f33ccfb5f72762d9c3898a34be6be0869 100644 (file)
@@ -82,6 +82,10 @@ struct OstreeSysroot
   /* The device/inode for / and /etc, used to detect booted deployment */
   dev_t root_device;
   ino_t root_inode;
+  /* The device inode for a queued soft reboot deployment */
+  gboolean have_nextroot;
+  dev_t nextroot_device;
+  ino_t nextroot_inode;
 
   // The parsed data from /run/ostree
   GVariantDict *run_ostree_metadata;
@@ -151,6 +155,8 @@ gboolean _ostree_sysroot_finalize_staged (OstreeSysroot *self, GCancellable *can
 gboolean _ostree_sysroot_boot_complete (OstreeSysroot *self, GCancellable *cancellable,
                                         GError **error);
 
+gboolean _ostree_prepare_soft_reboot (GError **error);
+
 OstreeDeployment *_ostree_sysroot_deserialize_deployment_from_variant (GVariant *v, GError **error);
 
 char *_ostree_sysroot_get_deployment_backing_relpath (OstreeDeployment *deployment);
index e45a71f689398ea3bac3988cec47d82403ca349d..f9955ce23375db3d3bd4507ddfd4a1cfa3648c07 100644 (file)
@@ -867,16 +867,16 @@ parse_deployment (OstreeSysroot *self, const char *boot_link, OstreeDeployment *
   if (!glnx_opendirat (self->sysroot_fd, relative_boot_link, TRUE, &deployment_dfd, error))
     return FALSE;
 
+  struct stat stbuf;
+  if (!glnx_fstat (deployment_dfd, &stbuf, error))
+    return FALSE;
+
   /* See if this is the booted deployment */
   const gboolean looking_for_booted_deployment
       = (self->root_is_ostree_booted && !self->booted_deployment);
   gboolean is_booted_deployment = FALSE;
   if (looking_for_booted_deployment)
     {
-      struct stat stbuf;
-      if (!glnx_fstat (deployment_dfd, &stbuf, error))
-        return FALSE;
-
       /* ostree-prepare-root records the (device, inode) pair of the underlying real deployment
        * directory (before we might have mounted a composefs or overlayfs on top).
        *
@@ -904,6 +904,9 @@ parse_deployment (OstreeSysroot *self, const char *boot_link, OstreeDeployment *
       is_booted_deployment
           = stbuf.st_dev == expected_root_dev && stbuf.st_ino == expected_root_inode;
     }
+  gboolean is_soft_reboot_target
+      = self->have_nextroot
+        && (stbuf.st_dev == self->nextroot_device && stbuf.st_ino == self->nextroot_inode);
 
   g_autoptr (OstreeDeployment) ret_deployment
       = ostree_deployment_new (-1, osname, treecsum, deployserial, bootcsum, treebootserial);
@@ -915,7 +918,6 @@ parse_deployment (OstreeSysroot *self, const char *boot_link, OstreeDeployment *
       ret_deployment, _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_FLAG_DEVELOPMENT);
   g_autofree char *unlocked_transient_path = _ostree_sysroot_get_runstate_path (
       ret_deployment, _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_FLAG_TRANSIENT);
-  struct stat stbuf;
   if (lstat (unlocked_development_path, &stbuf) == 0)
     ret_deployment->unlocked = OSTREE_DEPLOYMENT_UNLOCKED_DEVELOPMENT;
   else if (lstat (unlocked_transient_path, &stbuf) == 0)
@@ -932,6 +934,7 @@ parse_deployment (OstreeSysroot *self, const char *boot_link, OstreeDeployment *
         }
       /* TODO: warn on unknown unlock types? */
     }
+  ret_deployment->soft_reboot_target = is_soft_reboot_target;
 
   g_debug ("Deployment %s.%d unlocked=%d", treecsum, deployserial, ret_deployment->unlocked);
 
@@ -1137,7 +1140,18 @@ _ostree_sysroot_reload_staged (OstreeSysroot *self, GError **error)
   if (!self->root_is_ostree_booted)
     return TRUE; /* Note early return */
 
-  g_assert (self->booted_deployment);
+  /* In normal cases, we should have a booted deployment. However, during
+   * soft-reboot scenarios, the current deployment may not correspond to
+   * any bootloader entry, so booted_deployment could be NULL. */
+  if (!self->booted_deployment)
+    {
+      /* Check if we're in a soft-reboot scenario */
+      if (!(g_file_test ("/run/nextroot", G_FILE_TEST_IS_DIR)
+            && g_file_test ("/run/nextroot/sysroot", G_FILE_TEST_IS_DIR)))
+        {
+          g_assert (self->booted_deployment);
+        }
+    }
 
   g_clear_object (&self->staged_deployment);
   g_clear_pointer (&self->staged_deployment_data, g_variant_unref);
@@ -1190,6 +1204,44 @@ _ostree_sysroot_reload_staged (OstreeSysroot *self, GError **error)
   return TRUE;
 }
 
+/* Reload state from /run/ostree/nextroot-booted */
+static gboolean
+_ostree_sysroot_reload_soft_reboot (OstreeSysroot *self, GError **error)
+{
+  GLNX_AUTO_PREFIX_ERROR ("Loading nextroot", error);
+  // Reset state
+  self->have_nextroot = FALSE;
+
+  glnx_autofd int fd = -1;
+  if (!ot_openat_ignore_enoent (AT_FDCWD, OTCORE_RUN_NEXTROOT_BOOTED, &fd, error))
+    return FALSE;
+  // If there's no such file, we're done
+  if (fd == -1)
+    return TRUE;
+
+  // Parse the GVariant metadata from this; search for OTCORE_RUN_BOOTED_KEY_BACKING_ROOTDEVINO
+  // to find similar code.
+  g_autoptr (GVariant) metadata = NULL;
+  if (!ot_variant_read_fd (fd, 0, G_VARIANT_TYPE_VARDICT, TRUE, &metadata, error))
+    return glnx_prefix_error (error, "failed to read %s", OTCORE_RUN_NEXTROOT_BOOTED);
+
+  // Get the backing device/inode from metadata
+  guint64 backing_dev, backing_ino;
+  g_autoptr (GVariant) backing_devino = g_variant_lookup_value (
+      metadata, OTCORE_RUN_BOOTED_KEY_BACKING_ROOTDEVINO, G_VARIANT_TYPE ("(tt)"));
+  if (!backing_devino)
+    return glnx_throw (error, "Missing %s key in %s", OTCORE_RUN_BOOTED_KEY_BACKING_ROOTDEVINO,
+                       OTCORE_RUN_NEXTROOT_BOOTED);
+
+  // Load the device/inode, and we're done
+  g_variant_get (backing_devino, "(tt)", &backing_dev, &backing_ino);
+  self->have_nextroot = TRUE;
+  self->nextroot_device = (dev_t)backing_dev;
+  self->nextroot_inode = (ino_t)backing_ino;
+
+  return TRUE;
+}
+
 /* Loads the current bootversion, subbootversion, and deployments, starting from the
  * bootloader configs which are the source of truth.
  */
@@ -1208,6 +1260,9 @@ sysroot_load_from_bootloader_configs (OstreeSysroot *self, GCancellable *cancell
                                                     error))
     return FALSE;
 
+  if (!_ostree_sysroot_reload_soft_reboot (self, error))
+    return FALSE;
+
   g_autoptr (GPtrArray) boot_loader_configs = NULL;
   if (!_ostree_sysroot_read_boot_loader_configs (self, bootversion, &boot_loader_configs,
                                                  cancellable, error))
index 64a1207c0410b6c47ae37c9303a6b30192cff1ea..327c6d4515174c54c49de9943df3fa9823817231 100644 (file)
@@ -268,6 +268,15 @@ gboolean ostree_sysroot_simple_write_deployment (OstreeSysroot *sysroot, const c
                                                  OstreeSysrootSimpleWriteDeploymentFlags flags,
                                                  GCancellable *cancellable, GError **error);
 
+_OSTREE_PUBLIC gboolean ostree_sysroot_deployment_can_soft_reboot (OstreeSysroot *self,
+                                                                   OstreeDeployment *deployment);
+
+_OSTREE_PUBLIC gboolean ostree_sysroot_deployment_prepare_next_root (OstreeSysroot *self,
+                                                                     OstreeDeployment *deployment,
+                                                                     gboolean allow_kernel_skew,
+                                                                     GCancellable *cancellable,
+                                                                     GError **error);
+
 _OSTREE_PUBLIC
 gboolean ostree_sysroot_deployment_kexec_load (OstreeSysroot *self, OstreeDeployment *deployment,
                                                GCancellable *cancellable, GError **error);
index 01468b1f7a5785bde7b2bf5ca320fb344274f004..24b4d6cbf1a70a4abe3e8afe692541944f02e2d0 100644 (file)
@@ -133,9 +133,14 @@ gboolean otcore_mount_etc (GKeyFile *config, GVariantBuilder *metadata_builder,
 #define OTCORE_PREPARE_ROOT_KEYPATH_KEY "keypath"
 #define OTCORE_PREPARE_ROOT_TRANSIENT_KEY "transient"
 
+// For use with systemd soft reboots
+#define OTCORE_RUN_NEXTROOT "/run/nextroot"
+
 // The file written in the initramfs which contains an a{sv} of metadata
 // from ostree-prepare-root.
 #define OTCORE_RUN_BOOTED "/run/ostree-booted"
+// Written by ostree-soft-reboot.c with metadata about /run/nextroot
+#define OTCORE_RUN_NEXTROOT_BOOTED "/run/ostree/nextroot-booted"
 // This key will be present if composefs was successfully used.
 #define OTCORE_RUN_BOOTED_KEY_COMPOSEFS "composefs"
 // True if fsverity was required for composefs.
diff --git a/src/ostree/ot-admin-builtin-impl-prepare-soft-reboot.c b/src/ostree/ot-admin-builtin-impl-prepare-soft-reboot.c
new file mode 100644 (file)
index 0000000..c4d9553
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.0+
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+
+#include "ostree.h"
+#include "ot-admin-builtins.h"
+#include "ot-admin-functions.h"
+#include "otutil.h"
+
+#include "ostree-cmd-private.h"
+
+gboolean
+ot_admin_builtin_impl_prepare_soft_reboot (int argc, char **argv,
+                                           OstreeCommandInvocation *invocation,
+                                           GCancellable *cancellable, GError **error)
+{
+  if (!ostree_cmd__private__ ()->ostree_prepare_soft_reboot (error))
+    return FALSE;
+
+  return TRUE;
+}
diff --git a/src/ostree/ot-admin-builtin-prepare-soft-reboot.c b/src/ostree/ot-admin-builtin-prepare-soft-reboot.c
new file mode 100644 (file)
index 0000000..3151f6e
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2025 Colin Walters <walters@verbum.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.0+
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+
+#include "ostree.h"
+#include "ot-admin-builtins.h"
+#include "ot-admin-functions.h"
+#include "otutil.h"
+
+static GOptionEntry options[] = { { NULL } };
+
+gboolean
+ot_admin_builtin_prepare_soft_reboot (int argc, char **argv, OstreeCommandInvocation *invocation,
+                                      GCancellable *cancellable, GError **error)
+{
+  g_autoptr (GOptionContext) context = g_option_context_new ("INDEX");
+
+  g_autoptr (OstreeSysroot) sysroot = NULL;
+  if (!ostree_admin_option_context_parse (context, options, &argc, &argv,
+                                          OSTREE_ADMIN_BUILTIN_FLAG_SUPERUSER, invocation, &sysroot,
+                                          cancellable, error))
+    return FALSE;
+
+  if (argc < 2)
+    {
+      ot_util_usage_error (context, "INDEX must be specified", error);
+      return FALSE;
+    }
+
+  const char *deploy_index_str = argv[1];
+  guint deploy_index;
+  {
+    char *endptr = NULL;
+    errno = 0;
+    deploy_index = (guint)g_ascii_strtoull (deploy_index_str, &endptr, 10);
+    if (*endptr != '\0')
+      return glnx_throw (error, "Invalid index: %s", deploy_index_str);
+  }
+
+  g_autoptr (OstreeDeployment) target_deployment
+      = ot_admin_get_indexed_deployment (sysroot, deploy_index, error);
+  if (!target_deployment)
+    return FALSE;
+
+  if (target_deployment == ostree_sysroot_get_booted_deployment (sysroot))
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
+                   "Cannot prepare for soft-reboot currently booted deployment %i", deploy_index);
+      return FALSE;
+    }
+
+  if (!ostree_sysroot_deployment_prepare_next_root (sysroot, target_deployment, FALSE, cancellable,
+                                                    error))
+    return FALSE;
+
+  return TRUE;
+}
index d05d9928816ec76b1c15ae57d3353397bbed412e..485c8d9c22dd1e9e0a3a25dc9398893c4765886d 100644 (file)
@@ -41,6 +41,7 @@ static GOptionEntry options[]
           "Output \"default\" if booted into the default deployment, otherwise \"not-default\"",
           NULL },
         { NULL } };
+
 static gboolean
 deployment_print_status (OstreeSysroot *sysroot, OstreeRepo *repo, OstreeDeployment *deployment,
                          gboolean is_booted, gboolean is_pending, gboolean is_rollback,
@@ -77,18 +78,24 @@ deployment_print_status (OstreeSysroot *sysroot, OstreeRepo *repo, OstreeDeploym
   g_autofree char *origin_refspec
       = origin ? g_key_file_get_string (origin, "origin", "refspec", NULL) : NULL;
 
-  const char *deployment_status = "";
+  g_autoptr (GString) deployment_status = g_string_new ("");
+
   if (ostree_deployment_is_finalization_locked (deployment))
-    deployment_status = " (finalization locked)";
+    g_string_append (deployment_status, " (finalization locked)");
   else if (ostree_deployment_is_staged (deployment))
-    deployment_status = " (staged)";
+    g_string_append (deployment_status, " (staged)");
   else if (is_pending)
-    deployment_status = " (pending)";
+    g_string_append (deployment_status, " (pending)");
   else if (is_rollback)
-    deployment_status = " (rollback)";
-  g_print ("%c %s %s.%d%s\n", is_booted ? '*' : ' ', ostree_deployment_get_osname (deployment),
+    g_string_append (deployment_status, " (rollback)");
+
+  if (ostree_deployment_is_soft_reboot_target (deployment))
+    g_string_append (deployment_status, " (soft-reboot)");
+
+  char deployment_marker = is_booted ? '*' : ' ';
+  g_print ("%c %s %s.%d%s\n", deployment_marker, ostree_deployment_get_osname (deployment),
            ostree_deployment_get_csum (deployment), ostree_deployment_get_deployserial (deployment),
-           deployment_status);
+           deployment_status->str);
   if (version)
     g_print ("    Version: %s\n", version);
 
index 5e94b8a6c76d5c5950f7e4b660f5c88fb206afd8..f873e6bd9f54ca69209065c350d23ad66cf85dfe 100644 (file)
@@ -41,6 +41,8 @@ BUILTINPROTO (cleanup);
 BUILTINPROTO (pin);
 BUILTINPROTO (finalize_staged);
 BUILTINPROTO (boot_complete);
+BUILTINPROTO (prepare_soft_reboot);
+BUILTINPROTO (impl_prepare_soft_reboot);
 BUILTINPROTO (unlock);
 BUILTINPROTO (status);
 BUILTINPROTO (set_origin);
index 7488fe07a67321670effcf947a5682586606bde5..a89afb2bea8f0acd9835f5dfba156a0c634aeece 100644 (file)
@@ -42,6 +42,8 @@ static OstreeCommand admin_subcommands[] = {
     "Change the finalization locking state of the staged deployment" },
   { "boot-complete", OSTREE_BUILTIN_FLAG_NO_REPO | OSTREE_BUILTIN_FLAG_HIDDEN,
     ot_admin_builtin_boot_complete, "Internal command to run at boot after an update was applied" },
+  { "impl-prepare-soft-reboot", OSTREE_BUILTIN_FLAG_NO_REPO | OSTREE_BUILTIN_FLAG_HIDDEN,
+    ot_admin_builtin_impl_prepare_soft_reboot, "Internal command to prepare soft reboot" },
   { "state-overlay", OSTREE_BUILTIN_FLAG_NO_REPO | OSTREE_BUILTIN_FLAG_HIDDEN,
     ot_admin_builtin_state_overlay, "Internal command to assemble a state overlay" },
   { "init-fs", OSTREE_BUILTIN_FLAG_NO_REPO, ot_admin_builtin_init_fs,
@@ -57,6 +59,8 @@ static OstreeCommand admin_subcommands[] = {
     "rollback strings" },
   { "post-copy", OSTREE_BUILTIN_FLAG_NO_REPO, ot_admin_builtin_post_copy,
     "Update the repo and deployments as needed after a copy" },
+  { "prepare-soft-reboot", OSTREE_BUILTIN_FLAG_NO_REPO, ot_admin_builtin_prepare_soft_reboot,
+    "Prepare deployment for soft-reboot" },
   { "set-origin", OSTREE_BUILTIN_FLAG_NO_REPO, ot_admin_builtin_set_origin,
     "Set Origin and create a new origin file" },
   { "status", OSTREE_BUILTIN_FLAG_NO_REPO, ot_admin_builtin_status, "List deployments" },
diff --git a/tests/kolainst/destructive/soft-reboot.sh b/tests/kolainst/destructive/soft-reboot.sh
new file mode 100755 (executable)
index 0000000..25624c0
--- /dev/null
@@ -0,0 +1,59 @@
+#!/bin/bash
+set -xeuo pipefail
+
+. ${KOLA_EXT_DATA}/libinsttest.sh
+
+require_writable_sysroot
+prepare_tmpdir
+
+case "${AUTOPKGTEST_REBOOT_MARK:-}" in
+  "")
+  # xref https://github.com/coreos/coreos-assembler/pull/2814
+  systemctl mask --now zincati
+
+  assert_streq $(systemctl show -P SoftRebootsCount) 0
+
+  # Create a synthetic commit for upgrade
+  cd /ostree/repo/tmp
+  ostree checkout -H ${host_commit} t
+  unshare -m /bin/sh -c 'mount -o remount,rw /sysroot && cd /ostree/repo/tmp/t && touch usr/etc/new-file-for-soft-reboot usr/share/test-file-for-soft-reboot'
+  ostree commit --no-bindings --parent="${host_commit}" -b soft-reboot-test -I --consume t
+  newcommit=$(ostree rev-parse soft-reboot-test)
+  # Deploy the new commit normally first
+  ostree admin deploy --stage soft-reboot-test
+  
+  # Test prepare-soft-reboot command
+  echo "Testing prepare-soft-reboot..."
+  ostree admin prepare-soft-reboot 0
+  
+  ostree admin status > status.txt
+  assert_file_has_content_literal status.txt '(pending) (soft-reboot)'
+
+  test -f /run/ostree/nextroot-booted
+  
+  /tmp/autopkgtest-soft-reboot "2"
+  ;;
+  "2")
+  # After soft reboot, verify we're running the new deployment
+  echo "Verifying post-soft-reboot state..."
+  assert_streq $(systemctl show -P SoftRebootsCount) 1
+  
+  expected_commit=$(ostree rev-parse soft-reboot-test)
+  
+  if [ "${host_commit}" != "${expected_commit}" ]; then
+    echo "ERROR: Expected commit ${host_commit}, but got ${current_commit}"
+    exit 1
+  fi
+
+  test -f /etc/new-file-for-soft-reboot
+  test -f /usr/share/test-file-for-soft-reboot
+  
+  # Verify that soft-reboot-pending file is cleaned up
+  test '!' -f /run/ostree/nextroot-booted
+  
+  echo "Soft reboot test completed successfully!"
+  ;;
+  *) 
+  fatal "Unexpected AUTOPKGTEST_REBOOT_MARK=${AUTOPKGTEST_REBOOT_MARK}" 
+  ;;
+esac